<?php 
/**
 * This file is part of DomAr.
 * 
 * DomAr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * DomAr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with DomAr.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @link http://inditel.ee/
 * @copyright 2007-2009 Inditel Meedia OÜ
 * @author Oliver Leisalu <oliver@inditel.ee>
 */

/**
* @package DomAr
* @subpackage Relation
*/
class DomArManyToOne extends DomArProperty_Plugin implements 
																				IDomArPropertyAlterTableListener,
																				IDomArPropertyAfterInsertListener,
																				IDomArPropertyBeforeInsertListener,
																				IDomArPropertyAfterUpdateListener,
																				IDomArPropertyBeforeUpdateListener,
																				IDomArPropertyBeforeDeleteListener,
																				IDomArPropertySerializeListener {
	
	
	
	/**
	* @var String Child class name
	*/
	protected $childClassName;
	
	/**
	* @var String Parent class nams
	*/
	protected $className;
	
	/**
	* @var String Inversed property name
	*/
	protected $inverse;
	
	/**
	* @var Boolean
	*/
	protected $actAsParent = true;
	
	/**
	* @var Boolean
	*/
	protected $composition = false;
	/**
	* @var Array
	*/
	protected $itemValidator;
	/**
	* @var Array
	*/
	protected $listeners = array();
	/**
	 * Sets if relation items are ordered or not. Value should be order holding property name of child object.
	 *
	 * @var String
	 */
	protected $ordered;
	
	
	/**
	* @param String $className
	* @param String $name
	* @param String $childClassName
	* @param Boolean $composition
	* @param Boolean $actAsParent
	* @param String $inverse
	* @param Boolean $inverseToMany 
	*/
	public function __construct( $className, $name, $childClassName, $composition = false, $actAsParent = true, $inverse = NULL, $itemValidator = NULL, $ordered = NULL ) {
		parent::__construct( $name );
		$this->childClassName = $childClassName;
		$this->composition = (bool)$composition;
		$this->className = $className;
		$this->actAsParent = (bool)$actAsParent;
		$this->inverse = $inverse;
		$this->itemValidator = $itemValidator;
		$this->ordered = $ordered;
	}
	
	/**
	* @param String $type [beforeadd,afteradd,beforeremove,afterremove]
	* @param String $method
	*/
	public function registerListener( $type, $method, array $args ) {
		$this->listeners[$type][] = array($method, $args);
		//if( $args != NULL )
		//	throw new RuntimeException('Arguments are not allowed for plugin DomArManyToOne listeners!');
	}
	
	/**
	* @param DomArObject $object
	* @param Array $values
	* @return NULL
	*/
	public function init( DomArObject $object, array &$values ) {
		return NULL;
	}
	
	/**
	* @param DomArObject $object
	* @param NULL $propertyValue
	* @return DomArSet
	*/
	public function load( DomArObject $object, &$propertyValue ) {
		$class = ($this->ordered === NULL ? 'DomArSet' : 'DomArOrderedSet');
		return new $class( $object, $this );
	}
	
	/**
	* @param DomArObject $object
	* @param DomArSet $set
	* @return Boolean
	*/
	public function isLoaded( DomArObject $object, &$set ) {
		return $set instanceof DomArSet;
	}

	/**
	* @param DomArObject $object
	* @param Int|DomArObject $oldPropertyValue
	* @param Array $customParams
	* @return 1 - always fails because setting new DoMArSet is not allowed.
	*/
	public function validate( DomArObject $object, &$propertyValue, array &$customParams = array() ) {
		return spl_object_hash($propertyValue) == spl_object_hash($object->{$this->name}) ? 0 : 1;
	}
	
	/**
	* @param DomArObject $object
	* @param DomArSet $set
	* @return String
	*/
	public function beforeInsert( DomArSession $session, DomArObject $parent, &$set ) {
		return $this->beforeUpdate( $session, $parent, $set );
	}
	
	public function afterInsert( DomArObject $parent, &$set ) {
		if( $set ) {
			$this->afterUpdate( $parent, $set );
		}
	}
	
	/**
	* @param DomArObject $object
	* @param DomArSet $set
	* @return String
	*/
	public function beforeUpdate( DomArSession $session, DomArObject $parent, &$set ) {
		if( is_object($set) === true ) {
			
			foreach( $set->getNewObjects() as $object ) {
				$session->add( $object );
				//$session->addRelation( $parent, $object );
			}
			foreach( $set->getLoadedObjects() as $object ) {
				$session->add( $object );
			}
		}	
		return '';
	}
	
	public function afterUpdate(DomArObject $parent, &$set ) {
		if( !is_object($set) ) return;
		
		foreach( $set->getNewObjects() as $object ) {
			$set->addLoadedObject( $object );
		}
		$set->flushNewObjects();
	}
	
	/**
	* @param DomArTable $table Parent table.
	*/
	public function alterTable( DomArTable $table ) {		
		DomArTable::get( $this->childClassName )->update();		
	}
	
	/**
	* Removes parent from children if inversed or deletes children if composition
	* @param DomArObject $object
	* @param DomArObject $propertyValue
	*/
	public function beforeDelete( DomArObject $object, &$set ) {

		foreach( $set->query()->commit() as $childObject ) {

			$childObject->forceSet( $this->inverse, NULL );
			
			if( $this->composition === true )
				$childObject->delete();
			else
				DomSql::query('UPDATE '.$childObject->getTableName().' SET '.$this->inverse.' = NULL WHERE id = '.$childObject->id.' LIMIT 1');
			
		}
		
		$set->flushNewObjects();
		$set->flushLoadedObjects();
	}
	
	
	/**
	* @param DomArObject $object
	* @param DomArObject $propertyValue
	* @return DomArSet
	*/
	public function serialize( DomArObject $object, $set ) {	
		
		$result = NULL;
		
		if( is_object($set) && $object->isChanged($this->name) ) {
			
			$newItems = array();
			foreach( $set->getNewObjects() as $newObject ) {
				$newObject->_serializeData();
				$newItems[] = $newObject;
			}
			$loadedItems = array();
			foreach( $set->getLoadedObjects() as $loadedObject ) {
				if( !$loadedObject->getId() or $loadedObject->isChanged() ) {
					$loadedObject->_serializeData();
					$loadedItems[] = $loadedObject;
				}
			}
			
			$result = array($newItems, $loadedItems);
		}
		
		return $result;
	}
	
	/**
	* @param DomArObject $object
	* @param DomArObject $propertyValue
	* @return DomArSet
	*/
	public function unserialize( DomArObject $object, $propertyValue ) {

		if( is_array($propertyValue) ) {
			$null = NULL;
			$set = $this->load( $object, $null );
			
			// new objects in set
			if( !empty($propertyValue[0]) ) {
				foreach( $propertyValue[0] as $item ) {
					$item->_unserializeData();
					$set->addNewObject( $item );
				}
			}
			
			// loaded objects in set
			if( !empty($propertyValue[1]) ) {
				foreach( $propertyValue[1] as $item ) {
					$item->_unserializeData();
					$set->addLoadedObject( $item );
				}
			}
		
		} else {
			$set = NULL;
		}
		
		return $set;
	}
	
	
	
	
	
	
	
	
	
	/**
	* @return String
	*/
	public function getChildrenTableName() {
		return call_user_func(array($this->childClassName, 'getTableName'));
	}
	
	/**
	* @return String
	*/
	public function getChildrenDbTable() {
		return call_user_func(array($this->childClassName, 'getDbTable'));
	}
	
	/**
	* @return String
	*/
	public function getParentTableName() {
		return call_user_func(array($this->className, 'getTableName'));
	}
	
	/**
	* @return String
	*/
	public function getParentDbTable() {
		return call_user_func(array($this->className, 'getDbTable'));
	}
	
	
	public function invokeListener( $type, DomArObject $parent, DomArObject $child ) {
	
		if( $this->listeners[$type] ) {
			foreach( $this->listeners[$type] as $method ) {
				array_unshift( $method[1], $child );
				call_user_func_array( array($parent, $method[0]), $method[1] );
			}
		}
	}
	
	
	/**
	* @param DomArSet $set
	* @param DomArObject $object
	*/
	public function set_add( DomArSet $set, DomArObject $object ) {
		
		// test if already exists in set
		if( $object->id > 0 && $set->getParent()->id > 0 ) {
			$existsAlready = (bool)DomSql::selectOneQuery('SELECT IF(id>0,1,0) as r FROM '.$this->getChildrenDbTable().' as t WHERE t.id = '.(int)$object->id.' AND t.'.$this->inverse.'='.(int)$set->getParent()->id.' LIMIT 1','r');
			if( $existsAlready === true ) {
				$result = $set->addLoadedObject( $object );
				return;
			}
		} else if( $set->getParent()->id > 0 && $set->isChildNew( $object ) ) {
			return;
		}
		// ------
		
		$customParams = array();
		if( ($e = $this->set_validate( $set->getParent(), $object, $customParams, true )) !== 0 ) {
			$set->getParent()->registerFailedValidation( $e, $this->name, get_class($object), $customParams );
			return;
		}
		
		$this->invokeListener('beforeadd', $set->getParent(), $object );
			
		if( $this->inverse !== NULL) {
			$parent = $set->getParent();
			$object->_beforeSet( $this->inverse, $parent );
			$object->forceSet( $this->inverse,  $parent );
			$object->setChanged( $this->inverse, true );
			$object->_afterSet( $this->inverse, $parent );
		}

		$result = $set->addNewObject( $object );
			
		$this->invokeListener('afteradd', $set->getParent(), $object );
			
	}
	
	public function set_set( DomArSet $set, array $ids = NULL ) {
		
		if( $ids == NULL ) {
			foreach( $set->query()->commit() as $item ) {
				$this->set_remove( $set, $item );
			}
			return;
		}
		
		$uids = array();
		foreach( $ids as $k => $id ) {
			$uids[(is_object($id) ? $id->getId() : $id)] = true;
		}
		
		foreach( $set->query()->commit() as $item ) {
			if( isset($uids[$item->getId()]) === false )
				$this->set_remove( $set, $item );
			else
				unset($uids[$item->getId()]);
		}
		
		foreach( $uids as $id => $bool ) {
			if( $child = DomAr::load( $id, $this->childClassName ) )
				$this->set_add( $set, $child );
		}
	}
	
	/**
	* @param DomArSet $set
	* @param DomArObject $object
	* @param Boolean $rec If validation is checked for child object also.
	* @return Boolean
	*/
	public function set_validate( DomArObject $parent, DomArObject $object, array &$customParams = array(), $validateParentForChild = true ) {

		if( $this->inverse && $validateParentForChild === true) {
			return $object->_validate( $this->inverse, $parent, false, $customParams );
		
		} else {
			$validated = $object instanceof $this->childClassName  && call_user_func(array(get_class($object), 'getDbTable')) == call_user_func(array($this->childClassName, 'getDbTable')) ? 0 : 1;
			if( $validated !== 0) {
				return $validated;
			}
			
			if( $this->itemValidator[0] != '' ) {
				$result = (int)$parent->{$this->itemValidator[0]}( $object, $customParams );
				if( $result !== 0 ) {
					return $result;
				}
			}
		}
		
		return 0;
	}
	
	/**
	* @param DomArSet $set
	* @param Int $id
	* @return DomArObject
	*/
	public function set_find( DomArSet $set, $id ) {
		$r = $this->set_query( $set )->where('t.id='.(int)$id )->commit()->rewind();
		return $r->current();
	}
	/**
	* @param DomArSet $set
	* @return DomArObject
	*/
	public function set_query( DomArSet $set ) {
		return $this->prepareQuery( $set, DomArQuery::c( $set )->select( 't.*' ) );
	}
	
	/**
	* @param DomArSet $set
	* @param String $fields
	* @return DomArObject
	*/	
	public function set_dataQuery( DomArSet $set, $fields = 't.*' ) {
		return $this->prepareQuery( $set, DomArDataQuery::c( )->select( $fields ) );
	}
	
	/**
	* @param DomArSet $set
	* @param String $field
	* @return DomArObject
	*/	
	public function set_IdSetQuery( DomArSet $set, $field = 't.id' ) {
		return $this->prepareQuery( $set, DomArDataQuery::c()->select('t.id, '.addslashes($field).' as value')->setIndexColumnName('id')->setValueColumnName('value') );
	}
	
	
		
	/**
	* @param DomArSet $set
	* @return Int
	*/
	public function set_count(DomArSet $set ) {
		return (int)DomSql::selectOneQuery( 'SELECT count(*) as n FROM ' . $this->getChildrenDbTable().' WHERE `'.$this->inverse.'` = '.$set->getParent()->id , 'n');
	}
	
	/**
	* @param DomArSet $set
	* @param DomArObject $object
	*/
	public function set_remove( DomArSet $set, DomArObject $object ) {
		
		// before remove listeners
		$this->invokeListener('beforeremove', $set->getParent(), $object );
		// --------
		
		$set->unload( $object );
		
		if( $this->inverse !== NULL) {
			$null = NULL;
			$object->_beforeSet( $this->inverse, $null );
			$object->forceSet( $this->inverse,  $null );
			$object->setChanged( $this->inverse, true );
			$object->_afterSet( $this->inverse, $null );
			if( $object->id > 0 && $set->getParent()->id > 0 )
				$object->save();
		}
		
		if( $object->getId() > 0 && $set->getParent()->getId() > 0 ) 
			DomSql::query('UPDATE '.$this->getChildrenDbTable().' SET '.$this->inverse.' = NULL WHERE id = '.$object->getId().' LIMIT 1');
		
		
		// after remove listeners
		$this->invokeListener('afterremove', $set->getParent(), $object );	
		// --------
	}
	



	/**
	* Prepares search query for execution
	* @param DomArSet $set
	* @param DomArDataQuery $query
	*/
	protected function prepareQuery( DomArSet $set, DomSqlQuery $query  ){
		$parentId = $set->getParent()->id;
		$query->from( $this->getChildrenDbTable(),'t' )->where('t.'.$this->inverse.'='.($parentId > 0 ? (int)$parentId : 'NULL'));
		if( $query instanceof DomArQuery ) {
			$query->setClassName( $this->childClassName );
		}
		return $query;
	}
	
	
	/**
	* @param DomArObject $object
	* @param $propertyValue
	* @throws InvalidMethodCallException Cloning is currently not supported.
	*/
	public function _clone( DomArObject $object, &$propertyValue ) {
		throw new InvalidMethodCallException('Cloning lists is currenly not supported!');
	}

}
?>